﻿using System;
using System.Linq;
using System.Linq.Expressions;
using NHibernate;
using NHibernate.Event;
using NHibernate.Persister.Entity;
using Xenta.Entities;
using Xenta.Enums;
using Xenta.Operations;
using Xenta.Security;
using Xenta.Structs;

namespace Xenta.Utils
{
    /// <summary>
    /// Contains extension methods.
    /// </summary>
    public static class Extensions
    {
        #region IQueryable

        /// <summary>
        /// Bypasses a specified number of elements in a sequence and 
        /// takes the specified number of elements from remaining
        /// </summary>
        /// <typeparam name="TSource">The type of the elements of source.</typeparam>
        /// <param name="source">An sequence to return elements from.</param>
        /// <param name="index">The zero-based index. Use -1(negative) to take last N(count) elements.</param>
        /// <param name="count">The number of elements to take.</param>
        /// <returns>An sequence that contains elements.</returns>
        public static IQueryable<TSource> Paged<TSource>(this IQueryable<TSource> source, int index, int count)
        {
            count = Math.Max(1, count);
            if(index < 0)
                source = source.Skip(Math.Max(0, source.Count() - count));
            else
                source = source.Skip(index);
            return source.Take(count);
        }

        /// <summary>
        /// Filters elements in a sequence using property getter and predicate expressions.
        /// </summary>
        /// <typeparam name="TSource">The type of the elements of source.</typeparam>
        /// <typeparam name="TProperty">The type of property.</typeparam>
        /// <param name="source">An sequence to return elements from.</param>
        /// <param name="getter">The property getter expression.</param>
        /// <param name="predicate">The predicate expression.</param>
        /// <returns>An sequence that contains elements.</returns>
        public static IQueryable<TSource> Filter<TSource, TProperty>(this IQueryable<TSource> source,
            Expression<Func<TSource, TProperty>> getter,
            Expression<Func<TProperty, bool>> predicate)
        {
            return source.Where(Expression.Lambda<Func<TSource, bool>>(Expression.Invoke(predicate, Expression.Invoke(getter, getter.Parameters)), getter.Parameters));
        }

        /// <summary>
        /// Setups the flag mask based filter on a sequence of elements.
        /// </summary>
        /// <typeparam name="TSource">The type of the elements of source.</typeparam>
        /// <param name="source">A sequence of elements to filter.</param>
        /// <param name="getter">The flags value property getter. </param>
        /// <param name="flagMask">The flag mask.</param>
        /// <returns>An sequence of elements.</returns>
        /// <remarks>
        /// 
        /// Entity Flags Definition Rules(taken from the Solution/Documents/Dev Guide.txt file)
        ///
        /// - Must be System.Int32 based
        /// - Must have System.FlagsAttribute attribute
        /// - Must have zero value(e.g. None = 0x00000000)
        ///
        ///         0               1               2               3              
        ///  0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
        /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        /// |0|             O               |     G2    |     G1    |   S   |
        /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        /// 
        /// S  (4 bits, mask = 0x0000000f)  - base flags, e.g. activity flag, removal marker, etc. 
        /// G1 (6 bits, mask = 0x000003f0)  - typization flag group 1 
        /// G2 (6 bits, mask = 0x0000fc00)  - typization flag group 2 
        /// O  (15 bits, mask = 0x7fff0000) - option flags
        /// 
        /// In pursuance of these rules the algorithm of search is:
        /// 
        /// 1. Check if flag mask is zero, in this case no conditions need to be added to source
        /// 2. Checking the state bits
        /// 2.1. create the state mask, by applying the bitwise AND operation on a flag mask, using S mask
        /// 2.2. check if state mask is zero, in this case skip the 2.3
        /// 2.3. apply the bitwise AND operation on flags of elements and compare it for equality to state mask 
        /// 3. Checking the bits of group 1
        /// 3.1. create the group 1 mask, by applying the bitwise AND operation on a flag mask, using G1 mask
        /// 3.2. check if group 1 mask is zero, in this case skip the 3.3
        /// 3.3. apply the bitwise AND operation on flags of elements and check if the result is not zero
        /// 4. Checking the bits of group 2
        /// 4.1. create the group 2 mask, by applying the bitwise AND operation on a flag mask, using G2 mask
        /// 4.2. check if group 2 mask is zero, in this case skip the 4.3
        /// 4.3. apply the bitwise AND operation on flags of elements and check if the result is not zero
        /// 5. Checking the option bits
        /// 5.1. create the option mask, by applying the bitwise AND operation on a flag mask, using O mask
        /// 5.2. check if option mask is zero, in this case skip the 5.3
        /// 5.3. apply the bitwise AND operation on flags of elements and compare it for equality to option mask
        /// 
        /// </remarks>
        public static IQueryable<TSource> FilterByFlags<TSource>(this IQueryable<TSource> source,
            Expression<Func<TSource, int>> getter,
            int flagMask)
        {
            if(flagMask == 0)
                return source;
            //check S bits
            int stateMask = flagMask & FlagsHelper.FLAG_MASK_S;
            if(stateMask != 0)
                source = source.Filter(getter, x => (x & stateMask) == stateMask);
            //check G1 bits
            int g1Mask = flagMask & FlagsHelper.FLAG_MASK_G1;
            if(g1Mask != 0)
                source = source.Filter(getter, x => (x & g1Mask) != 0);
            //check G2 bits
            int g2Mask = flagMask & FlagsHelper.FLAG_MASK_G2;
            if(g2Mask != 0)
                source = source.Filter(getter, x => (x & g2Mask) != 0);
            //check O bits
            int optionMask = flagMask & FlagsHelper.FLAG_MASK_O;
            if(optionMask != 0)
                source = source.Filter(getter, x => (x & optionMask) == optionMask);
            return source;
        }

        /// <summary>
        /// Filters elements in a sequence using date property getter and date range.
        /// </summary>
        /// <typeparam name="TSource">The type of the elements of source.</typeparam>
        /// <param name="source">An sequence to return elements from.</param>
        /// <param name="getter">The property getter expression.</param>
        /// <param name="dtFrom">The date time "from" or null.</param>
        /// <param name="dtTo">The date time "to" or null.</param>
        /// <returns>An sequence that contains elements.</returns>
        public static IQueryable<TSource> FilterByDateRange<TSource>(this IQueryable<TSource> source,
            Expression<Func<TSource, DateTime>> getter,
            DateTime? dtFrom, DateTime? dtTo)
        {
            if(DateHelper.IsRangeValid(dtFrom, dtTo))
            {
                if(dtFrom.HasValue)
                    source = source.Filter(getter, x => x >= dtFrom.Value);
                if(dtTo.HasValue)
                    source = source.Filter(getter, x => x <= dtTo.Value);
            }
            return source;
        }

        /// <summary>
        /// Filters elements in a sequence using numeric property getter and numeric range.
        /// </summary>
        /// <typeparam name="TSource">The type of the elements of source.</typeparam>
        /// <param name="source">An sequence to return elements from.</param>
        /// <param name="getter">The property getter expression.</param>
        /// <param name="numFrom">The value "from" or null.</param>
        /// <param name="numTo">The value "to" or null.</param>
        /// <returns>An sequence that contains elements.</returns>
        public static IQueryable<TSource> FilterByNumRange<TSource>(this IQueryable<TSource> source,
            Expression<Func<TSource, decimal>> getter,
            Decimal? numFrom, Decimal? numTo)
        {
            if(NumericHelper.IsRangeValid(numFrom, numTo))
            {
                if(numFrom.HasValue)
                    source = source.Filter(getter, x => x >= numFrom.Value);
                if(numTo.HasValue)
                    source = source.Filter(getter, x => x <= numTo.Value);
            }
            return source;
        }

        /// <summary>
        /// Sorts the elements of a sequence in required order according to a key.
        /// </summary>
        /// <typeparam name="TSource">The type of the elements of source.</typeparam>
        /// <typeparam name="TKey">The type of the key returned by the function that is represented by keySelector.</typeparam>
        /// <param name="source">A sequence of values to order.</param>
        /// <param name="keySelector">A function to extract a key from an element.</param>
        /// <param name="sortDir">The sort direction.</param>
        /// <param name="subsequent">A value indicates whether to perform a subsequent ordering.</param>
        /// <returns>An IOrderedQueryable whose elements are sorted according to a key.</returns>
        public static IOrderedQueryable<TSource> OrderBy<TSource, TKey>(this IQueryable<TSource> source,
            Expression<Func<TSource, TKey>> keySelector,
            SortDirection sortDir,
            bool subsequent)
        {
            return subsequent && (source is IOrderedQueryable<TSource>)
                ? (source as IOrderedQueryable<TSource>).ThenBy(keySelector, sortDir)
                : source.OrderBy(keySelector, sortDir);
        }

        /// <summary>
        /// Sorts the elements of a sequence in required order according to a key.
        /// </summary>
        /// <typeparam name="TSource">The type of the elements of source.</typeparam>
        /// <typeparam name="TKey">The type of the key returned by the function that is represented by keySelector.</typeparam>
        /// <param name="source">A sequence of values to order.</param>
        /// <param name="keySelector">A function to extract a key from an element.</param>
        /// <param name="sortDir">The sort direction.</param>
        /// <returns>An IOrderedQueryable whose elements are sorted according to a key.</returns>
        public static IOrderedQueryable<TSource> OrderBy<TSource, TKey>(this IQueryable<TSource> source,
            Expression<Func<TSource, TKey>> keySelector,
            SortDirection sortDir)
        {
            return sortDir == SortDirection.Descending 
                ? source.OrderByDescending(keySelector) 
                : source.OrderBy(keySelector);
        }

        /// <summary>
        /// Sorts the elements of a sequence in required order according to a key.
        /// </summary>
        /// <typeparam name="TSource">The type of the elements of source.</typeparam>
        /// <typeparam name="TKey">The type of the key returned by the function that is represented by keySelector.</typeparam>
        /// <param name="source">A sequence of values to order.</param>
        /// <param name="keySelector">A function to extract a key from an element.</param>
        /// <param name="sortDir">The sourt direction.</param>
        /// <returns>An IOrderedQueryable whose elements are sorted according to a key.</returns>
        public static IOrderedQueryable<TSource> ThenBy<TSource, TKey>(this IOrderedQueryable<TSource> source,
            Expression<Func<TSource, TKey>> keySelector,
            SortDirection sortDir)
        {
            return sortDir == SortDirection.Descending 
                ? source.ThenByDescending(keySelector) 
                : source.ThenBy(keySelector);
        }

        #endregion

        #region IUnitOfWork

        /// <summary>
        /// Creates an entity in the datastore.
        /// </summary>
        /// <typeparam name="TEntity">The type of entity.</typeparam>
        /// <param name="uow">The unit of work.</param>
        /// <param name="entity">The entity.</param>
        /// <returns>The entity identifier.</returns>
        public static object CreateStateless<TEntity>(this IUnitOfWork uow, TEntity entity)
            where TEntity : IEntity
        {
            return uow.Create(entity, true);
        }

        /// <summary>
        /// Updates a persistent entity in the datastore.
        /// </summary>
        /// <typeparam name="TEntity">The type of entity.</typeparam>
        /// <param name="uow">The unit of work.</param>
        /// <param name="entity">The entity.</param>
        public static void UpdateStateless<TEntity>(this IUnitOfWork uow, TEntity entity)
            where TEntity : IEntity
        {
            uow.Update(entity, true);
        }

        /// <summary>
        /// Removes a persistent entity from the datastore.
        /// </summary>
        /// <typeparam name="TEntity">The type of entity.</typeparam>
        /// <param name="uow">The unit of work.</param>
        /// <param name="entity">The entity.</param>
        public static void DeleteStateless<TEntity>(this IUnitOfWork uow, TEntity entity)
            where TEntity : IEntity
        {
            uow.Delete(entity, true);
        }

        /// <summary>
        /// Gets an entity by the identifier.
        /// </summary>
        /// <typeparam name="TEntity">The type of entity.</typeparam>
        /// <param name="uow">The unit of work.</param>
        /// <param name="entityID">The entity identifier.</param>
        /// <returns>The entity if it found in the datastore; otherwise null.</returns>
        public static TEntity GetStateless<TEntity>(this IUnitOfWork uow, object entityID)
            where TEntity : IEntity
        {
            return uow.Get<TEntity>(entityID, true);
        }

        /// <summary>
        /// Refresh an entity state.
        /// </summary>
        /// <typeparam name="TEntity">The type of entity.</typeparam>
        /// <param name="uow">The unit of work.</param>
        /// <param name="entity">The entity.</param>
        public static void RefreshStateless<TEntity>(this IUnitOfWork uow, TEntity entity)
            where TEntity : IEntity
        {
            uow.Refresh(entity, true);
        }

        /// <summary>
        /// Query over the entity collection.
        /// </summary>
        /// <typeparam name="TEntity">The type of entity.</typeparam>
        /// <param name="uow">The unit of work.</param>
        /// <returns>The queryable entity collection.</returns>
        public static IQueryable<TEntity> QueryStateless<TEntity>(this IUnitOfWork uow)
            where TEntity : IEntity
        {
            return uow.Query<TEntity>(true);
        }

        /// <summary>
        /// Creates an ORM query.
        /// </summary>
        /// <param name="uow">The unit of work.</param>
        /// <param name="queryString">The query string.</param>
        /// <returns>The ORM query.</returns>
        public static IQuery OrmQueryStateless(this IUnitOfWork uow, string queryString)
        {
            return uow.OrmQuery(queryString, true);
        }

        /// <summary>
        /// Creates a SQL query.
        /// </summary>
        /// <param name="uow">The unit of work.</param>
        /// <param name="queryString">The query string.</param>
        /// <returns>The SQL query.</returns>
        public static ISQLQuery SqlQueryStateless(this IUnitOfWork uow, string queryString)
        {
            return uow.SqlQuery(queryString, true);
        }

        /// <summary>
        /// Executes the operation on a unit.
        /// </summary>
        /// <typeparam name="TOperation">The type of operation.</typeparam>
        /// <param name="uow">The unit of work.</param>
        /// <param name="operation">The operation.</param>
        /// <returns>The executed operation.</returns>
        public static TOperation Execute<TOperation>(this IUnitOfWork uow, TOperation operation)
            where TOperation : IOperation
        {
            operation.Execute(uow);
            return operation;
        }

        #endregion

        #region SecurityPolicy

        /// <summary>
        /// Adds the security rule to a security policy.
        /// </summary>
        /// <param name="policy">The security policy.</param>
        /// <param name="predicate">The predicate function.</param>
        /// <returns>The security policy.</returns>
        public static SecurityPolicy Rule(this SecurityPolicy policy, Func<IWorkingContext, IUnitOfWork, bool> predicate)
        {
            if(predicate != null)
                policy.Add(new SecurityRule(predicate));
            return policy;
        }

        #endregion

        #region IOperationProcessor
        
        /// <summary>
        /// Initializes the operation processor.
        /// </summary>
        /// <param name="processor">The operation processor.</param>
        /// <param name="ctx">The working context.</param>
        public static IOperationProcessor Init(this IOperationProcessor processor, IWorkingContext ctx)
        {
            processor.Initialize(ctx);
            return processor;
        }

        #endregion

        #region PostUpdateEvent

        /// <summary>
        /// Gets the old property value.
        /// </summary>
        /// <param name="evt">The update event object.</param>
        /// <param name="porpertyName">The name of a property.</param>
        /// <returns>The old value.</returns>
        public static object GetOldPropertyValue(this PostUpdateEvent evt, string porpertyName)
        {
            if(evt.OldState == null)
                return null;

            int propertyIndex = evt.Persister.GetPropertyIndex(porpertyName);

            if(propertyIndex == -1)
                return null;

            return evt.OldState[propertyIndex];
        }

        /// <summary>
        /// Sets the new property value.
        /// </summary>
        /// <param name="evt">The update event object.</param>
        /// <param name="porpertyName">The name of a property.</param>
        /// <param name="value">The value objet.</param>
        public static void SetNewPropertyValue(this PostUpdateEvent evt, string porpertyName, object value)
        {
            int propertyIndex = evt.Persister.GetPropertyIndex(porpertyName);

            if(propertyIndex == -1)
                return;
            evt.State[propertyIndex] = value;
        }

        #endregion

        #region PreUpdateEvent

        /// <summary>
        /// Gets the old property value.
        /// </summary>
        /// <param name="evt">The update event object.</param>
        /// <param name="porpertyName">The name of a property.</param>
        /// <returns>The old value.</returns>
        public static object GetOldPropertyValue(this PreUpdateEvent evt, string porpertyName)
        {
            if(evt.OldState == null)
                return null;

            int propertyIndex = evt.Persister.GetPropertyIndex(porpertyName);

            if(propertyIndex == -1)
                return null;

            return evt.OldState[propertyIndex];
        }

        /// <summary>
        /// Sets the new property value.
        /// </summary>
        /// <param name="evt">The update event object.</param>
        /// <param name="porpertyName">The name of a property.</param>
        /// <param name="value">The value objet.</param>
        public static void SetNewPropertyValue(this PreUpdateEvent evt, string porpertyName, object value)
        {
            int propertyIndex = evt.Persister.GetPropertyIndex(porpertyName);

            if(propertyIndex == -1)
                return;
            evt.State[propertyIndex] = value;
        }

        #endregion

        #region PreInsertEvent

        /// <summary>
        /// Sets the new property value.
        /// </summary>
        /// <param name="evt">The insert event object.</param>
        /// <param name="porpertyName">The name of a property.</param>
        /// <param name="value">The value objet.</param>
        public static void SetNewPropertyValue(this PreInsertEvent evt, string porpertyName, object value)
        {
            int propertyIndex = evt.Persister.GetPropertyIndex(porpertyName);

            if(propertyIndex == -1)
                return;
            evt.State[propertyIndex] = value;
        }

        #endregion

        #region IEntityPersister

        /// <summary>
        /// Gets the index of property in an entity persister.
        /// </summary>
        /// <param name="persister">The entity persister.</param>
        /// <param name="porpertyName">The name of a property.</param>
        /// <returns>The zero-based index, if found; otherwise, –1.</returns>
        public static int GetPropertyIndex(this IEntityPersister persister, string porpertyName)
        {
            return Array.IndexOf(persister.PropertyNames, porpertyName);
        }

        #endregion

        #region IDependencyResolver

        /// <summary>
        /// Resolves the dependency by identifier.
        /// </summary>
        /// <param name="ioc">The dependency resolver.</param>
        /// <param name="id">The identifier.</param>
        /// <returns>The dependency instance.</returns>
        public static IDependency Resolve(this IDependencyResolver ioc, Guid id)
        {
            return ioc.Resolve(id.ToString());
        }

        #endregion

        #region IInfrastructure

        /// <summary>
        /// Determines, if the dependency is registered.
        /// </summary>
        /// <param name="infrastructure">The infrastructure instance.</param>
        /// <param name="id">The dependency identifier.</param>
        /// <returns>true if dependency is registered; otherwise false.</returns>
        public static bool CheckDependency(this IInfrastructure infrastructure, Guid id)
        {
            return infrastructure.CheckDependency(id.ToString());
        }

        /// <summary>
        /// Determines, if dependency is registered and implements the 
        /// required service interface.
        /// </summary>
        /// <typeparam name="TDependency">The service interface to check.</typeparam>
        /// <param name="infrastructure">The infrastructure instance.</param>
        /// <param name="id">The identifier of required dependency.</param>
        /// <returns>
        /// true if dependency is registered and implements the 
        /// required interface; otherwise false.
        /// </returns>
        public static bool CheckDependency<TDependency>(this IInfrastructure infrastructure, Guid id)
            where TDependency : IDependency
        {
            return infrastructure.CheckDependency<TDependency>(id.ToString());
        }

        /// <summary>
        /// Gets the dependency.
        /// </summary>
        /// <param name="infrastructure">The infrastructure instance.</param>
        /// <param name="id">The dependency identifier.</param>
        /// <returns>The dependency info or null.</returns>
        public static DependencyInfo? DependencyInfo(this IInfrastructure infrastructure, Guid id)
        {
            return infrastructure.DependencyInfo(id.ToString());
        }

        #endregion
    }
}
